package com.only.fx;

import java.awt.Dimension;
import java.awt.Frame;
import java.awt.MouseInfo;
import java.awt.Point;
import java.awt.Toolkit;
import java.awt.Window;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.ComponentListener;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.WindowAdapter;
import java.awt.event.WindowEvent;
import java.awt.event.WindowStateListener;

import javax.swing.Timer;

public class WindowAutoHide implements MouseMotionListener, ActionListener, ComponentListener, WindowStateListener {

	public static final int HIDE_DIRECTION_LEFT = 1;
	public static final int HIDE_DIRECTION_RIGHT = 1 << 1;
	public static final int HIDE_DIRECTION_TOP = 1 << 2;
	public static final int HIDE_DIRECTION_BOTTOM = 1 << 3;
	public static final int HIDE_DIRECTION_ALL = HIDE_DIRECTION_LEFT | HIDE_DIRECTION_RIGHT | HIDE_DIRECTION_TOP | HIDE_DIRECTION_BOTTOM;
	private Window window;
	private HiddenPosition hiddenPos;
	private Timer hideTimer, mouseTimer;
	private Point oldLocation, newLocation, tempLocation;
	private boolean mouseIn;
	private int step;
	private int rim;
	private int hideDelay;
	private int hideDirection;
	private boolean limitLocation;
	private boolean force;
	private boolean listenerAdded;

	public WindowAutoHide(final Window window) {
		this.window = window;
		this.step = 10;
		this.rim = 2;
		this.hideDelay = 230;
		this.hideDirection = HIDE_DIRECTION_LEFT | HIDE_DIRECTION_RIGHT | HIDE_DIRECTION_TOP;
		this.limitLocation = true;
		this.hideTimer = new Timer(0, this);
		this.mouseTimer = new Timer(100, this);
		this.newLocation = new Point();
		this.tempLocation = new Point();
		hideTimer.setDelay(1);
		mouseTimer.setInitialDelay(1000);
		window.addWindowStateListener(this);
		window.addWindowListener(new WindowAdapter() {
			@Override
			public void windowDeactivated(WindowEvent e) {
				forceHide();
			}
		});

		if (window.isVisible()) {
			componentShown(null);
		} else {
			window.addComponentListener(new ComponentAdapter() {
				@Override
				public void componentShown(ComponentEvent e) {
					window.removeComponentListener(this);
					WindowAutoHide.this.componentShown(null);
				}
			});
		}
	}

	private void hideOrShow() {
		Point location = window.getLocation();
		double deltaX = newLocation.getX() - oldLocation.getX();
		double deltaY = newLocation.getY() - oldLocation.getY();
		boolean finished = false;

		if (deltaX == 0) {
			if (deltaY < 0) {
				tempLocation.setLocation(location.getX(), location.getY() - step);

				if (tempLocation.getY() <= newLocation.getY()) {
					tempLocation.setLocation(newLocation);
					finished = true;
				}
			} else {
				tempLocation.setLocation(location.getX(), location.getY() + step);

				if (tempLocation.getY() >= newLocation.getY()) {
					tempLocation.setLocation(newLocation);
					finished = true;
				}
			}

			window.setLocation(tempLocation);
		} else if (deltaY == 0) {
			if (deltaX < 0) {
				tempLocation.setLocation(location.getX() - step, location.getY());

				if (tempLocation.getX() <= newLocation.getX()) {
					tempLocation.setLocation(newLocation);
					finished = true;
				}
			} else {
				tempLocation.setLocation(location.getX() + step, location.getY());

				if (tempLocation.getX() >= newLocation.getX()) {
					tempLocation.setLocation(newLocation);
					finished = true;
				}
			}

			window.setLocation(tempLocation);
		}

		if (finished) {
			stopHideTimer();
		}
	}

	public void forceHide() {
		if (mouseTimer.isRunning()) {
			force = true;
			mouseExited();
		}
	}

	public void forceShow() {
		if (mouseTimer.isRunning()) {
			force = true;
			mouseEntered();
		}
	}

	private void addOrRemoveListener(boolean add) {
		if (!add && listenerAdded) {
			window.removeMouseMotionListener(this);
			window.removeComponentListener(this);
			listenerAdded = false;
		} else if (add && !listenerAdded) {
			window.addMouseMotionListener(this);
			window.addComponentListener(this);
			listenerAdded = true;
		}
	}

	private void mouseExited() {
		checkTimer();
		mouseIn = false;
		hideTimer.setInitialDelay(hideDelay);
		Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();

		if (window.getX() <= 0 && (hideDirection & HIDE_DIRECTION_LEFT) != 0) {
			oldLocation = window.getLocation();
			newLocation.setLocation(-window.getWidth() + rim, window.getY());
			hiddenPos = HiddenPosition.LEFT;
			startHideTimer();
		} else if (window.getX() >= screenSize.getWidth() - window.getWidth() && (hideDirection & HIDE_DIRECTION_RIGHT) != 0) {
			oldLocation = window.getLocation();
			newLocation.setLocation(screenSize.getWidth() - rim, window.getY());
			hiddenPos = HiddenPosition.RIGHT;
			startHideTimer();
		} else if (window.getY() <= 0 && (hideDirection & HIDE_DIRECTION_TOP) != 0) {
			oldLocation = window.getLocation();
			newLocation.setLocation(window.getX(), -window.getHeight() + rim);
			hiddenPos = HiddenPosition.TOP;
			startHideTimer();
		} else if (window.getY() >= screenSize.getHeight() - window.getHeight() && (hideDirection & HIDE_DIRECTION_BOTTOM) != 0) {
			oldLocation = window.getLocation();
			newLocation.setLocation(window.getX(), screenSize.getHeight() - rim);
			hiddenPos = HiddenPosition.BOTTOM;
			startHideTimer();
		} else {
			force = false;
			hiddenPos = null;
		}
	}

	private void mouseEntered() {
		checkTimer();
		mouseIn = true;
		hideTimer.setInitialDelay(0);

		if (hiddenPos != null && hideDirection > 0) {
			oldLocation = window.getLocation();

			if (hiddenPos == HiddenPosition.LEFT && (hideDirection & HIDE_DIRECTION_LEFT) != 0) {
				newLocation.setLocation(0, window.getY());
				startHideTimer();
			} else if (hiddenPos == HiddenPosition.RIGHT && (hideDirection & HIDE_DIRECTION_RIGHT) != 0) {
				Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
				newLocation.setLocation(screenSize.getWidth() - window.getWidth(), window.getY());
				startHideTimer();
			} else if (hiddenPos == HiddenPosition.TOP && (hideDirection & HIDE_DIRECTION_TOP) != 0) {
				newLocation.setLocation(window.getX(), 0);
				startHideTimer();
			} else if (hiddenPos == HiddenPosition.BOTTOM && (hideDirection & HIDE_DIRECTION_BOTTOM) != 0) {
				Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
				newLocation.setLocation(window.getX(), screenSize.getHeight() - window.getHeight());
				startHideTimer();
			} else {
				force = false;
			}
		} else {
			force = false;
		}
	}

	private void checkTimer() {
		if (hideTimer.isRunning()) {
			stopHideTimer();
		}
	}

	private void startHideTimer() {
		if (force) {
			mouseTimer.stop();
		}

		hideTimer.start();
	}

	private void stopHideTimer() {
		hideTimer.stop();

		if (mouseIn) {
			hiddenPos = null;
		}

		if (force) {
			force = false;
			Point mouseLocation = MouseInfo.getPointerInfo().getLocation();
			this.mouseIn = window.getBounds().contains(mouseLocation);
			int winState = Frame.NORMAL;

			if (window instanceof Frame) {
				winState = ((Frame) window).getExtendedState();
			}

			if (window.isVisible() && winState == Frame.NORMAL) {
				mouseTimer.start();
			}
		}
	}

	public boolean limitLocationAllowed() {
		return hiddenPos == null;
	}

	public int getStep() {
		return this.step;
	}

	public void setStep(int step) {
		this.step = step;
	}

	public int getRim() {
		return this.rim;
	}

	public void setRim(int rim) {
		this.rim = rim;
	}

	public int getHideDelay() {
		return this.hideDelay;
	}

	public void setHideDelay(int hideDelay) {
		this.hideDelay = hideDelay;
	}

	public int getHideDirection() {
		return this.hideDirection;
	}

	public void setHideDirection(int hideDirection) {
		this.hideDirection = hideDirection;
	}

	public boolean isLimitLocation() {
		return this.limitLocation;
	}

	public void setLimitLocation(boolean limitLocation) {
		this.limitLocation = limitLocation;
	}

	public void windowStateChanged(WindowEvent e) {
		hiddenPos = null;
		mouseIn = false;
		int newState = e.getNewState();

		if (newState == Frame.NORMAL) {
			addOrRemoveListener(true);

			if ((e.getOldState() & Frame.ICONIFIED) != 0) {
				componentMoved(null);
			}

			if (!mouseTimer.isRunning()) {
				mouseTimer.start();
			}
		} else if (newState != Frame.NORMAL) {
			addOrRemoveListener(false);

			if (mouseTimer.isRunning()) {
				mouseTimer.stop();
			}
		}
	}

	public void actionPerformed(ActionEvent e) {
		Object source = e.getSource();

		if (window.isVisible()) {
			if (source == mouseTimer) {
				mouseMoved(null);
			} else if (source == hideTimer) {
				hideOrShow();
			}
		} else {
			componentHidden(null);
		}
	}

	public void mouseDragged(MouseEvent e) {
		hiddenPos = null;
	}

	public void mouseMoved(MouseEvent e) {
		if (e == null) {
			Point mouseLocation = MouseInfo.getPointerInfo().getLocation();
			boolean mouseIn = window.getBounds().contains(mouseLocation);

			if (this.mouseIn != mouseIn) {
				if (mouseIn) {
					mouseEntered();
				} else if (!mouseIn) {
					mouseExited();
				}
			}
		}
	}

	public void componentMoved(ComponentEvent e) {
		if (window instanceof Frame && ((Frame) window).getExtendedState() != Frame.NORMAL) {
			return;
		}

		if (e == null || (limitLocation && hiddenPos == null)) {
			Dimension screenSize = Toolkit.getDefaultToolkit().getScreenSize();
			int deltaWidth = screenSize.width - window.getWidth();
			int deltaHeight = screenSize.height - window.getHeight();

			if (window.getX() < 0 && (hideDirection & HIDE_DIRECTION_LEFT) != 0) {
				window.setLocation(0, window.getY());
			} else if (window.getX() > deltaWidth && (hideDirection & HIDE_DIRECTION_RIGHT) != 0) {
				window.setLocation(deltaWidth, window.getY());
			} else if (window.getY() < 0 && (hideDirection & HIDE_DIRECTION_TOP) != 0) {
				window.setLocation(window.getX(), 0);
			} else if (window.getY() > deltaHeight && (hideDirection & HIDE_DIRECTION_BOTTOM) != 0) {
				window.setLocation(window.getX(), deltaHeight);
			}
		}
	}

	public void componentShown(ComponentEvent e) {
		int winState = Frame.NORMAL;

		if (window instanceof Frame) {
			winState = ((Frame) window).getExtendedState();
		}

		if (winState == Frame.NORMAL) {
			addOrRemoveListener(true);

			if (!mouseTimer.isRunning()) {
				mouseTimer.start();
			}
		}
	}

	@Override
	public void componentHidden(ComponentEvent e) {
		addOrRemoveListener(false);

		if (mouseTimer.isRunning()) {
			mouseTimer.stop();
		}
	}

	@Override
	public void componentResized(ComponentEvent e) {
	}

	private static enum HiddenPosition {

		LEFT, RIGHT, TOP, BOTTOM
	}
}